機械学習モデルBodyPixを使ってAmazon Chime SDKのビデオ会議の背景をぼかしてみた(前編)
こんにちは、CX事業本部の若槻です。
AWSのコラボレーションツールであるAmazon Chimeでは、2020/11現在のところ、ビデオ会議で人物の背景のみをぼかす「背景ぼかし機能」は提供されておらず、同様にAmazon Chime SDKでも標準の実装としては未提供となります。
そこで今回は、ない機能は自分で作っちゃおうということで、オープンソースの機械学習モデルであるBodyPixを使ってAmazon Chime SDKのビデオ会議の背景をぼかせることを簡単に確認してみました。
BodyPixとは
BodyPixとは、ブラウザとTensorFlow.jsを使って人や体の部位が映っている領域を分割(セグメンテーション)できるGoogleのオープンソース機械学習モデルです。
ちなみに、このBodyPixを使用してAmazon Chime SDKのビデオ会議のバーチャル背景を実装している方も既にいらっしゃいます。
アウトプット
Amazon Chime SDKへの背景ぼかし機能の実装は、AWS公式が公開している下記のデモアプリ(コミットバージョン39a4e73)を修正する形で行いました。
実際にビデオ会議アプリとして使うには不完全ですが、「videoタグ領域」と「canvasタグ領域」を会議実施画面に追加して、BodyPixによる背景ぼかしを行い、Amazon Chime SDKの「ビデオタイル」に出力するようにしています。処理のシーケンスは次のようになります。
- カメラからvideoタグ領域への映像入力
- videoタグ領域の映像の背景をぼかし処理してcanvasタグ領域へ描画
- canvasタグ領域の描画をMediaStreamとして取得しビデオタイルに入力する
ビデオ会議の背景のぼかしが行われている様子です。少し粗くて見にくいですが、身体の動きに合わせてぼかし領域もちゃんと追随できています。
やってみる
Amazon Chime SDKのデモアプリのソースコードのダウンロード
% git clone https://github.com/aws/amazon-chime-sdk-js.git % cd amazon-chime-sdk-js/demos/browser
AWSの認証
今回使用する/demos/browser
のデモアプリはローカルで動作するため、バックエンドを構築する必要はありません。ただし、
会議利用時にアプリが会議の作成、削除、出席者の作成をするためにAmazon Chime APIにアクセスするため、その際にAWSの認証情報が必要となります。まだの場合は認証を設定してください。
修正前のデモアプリの確認
npm run start
を実行して、依存関係のインストールとアプリのローカルでの起動を行います。
% npm run start
ターミナルに次のように表示されたらアプリの起動は成功です。
2020-11-11T13:04:42.423Z server running at http://127.0.0.1:8080/
http://127.0.0.1:8080/
にアクセスすると会議参加画面が開きます。
デバイス準備画面です。
修正前のデモアプリの会議実施画面です。ビデオ会議に参加できています。
デモアプリを修正してビデオ会議の背景をぼかす
BodyPixの依存関係のインストール
% npm install @tensorflow-models/body-pix @tensorflow/tfjs-converter @tensorflow/tfjs-core @tensorflow/tfjs
それぞれ次の用途でインストールしています。
@tensorflow-models/body-pix
:BodyPix本体。@tensorflow/tfjs-converter
と@tensorflow/tfjs-core
:インストールせずにnpm run start
を実行するとエラーとなりこれらのインストールを促されたため。@tensorflow/tfjs
:インストールせずにアプリ上でBodyPixによる処理を行おうとするとエラーとなるため。(詳細は後述)
「videoタグ領域」と「canvasタグ領域」の実装
demos/browser/app/meetingV2/meetingV2.html
の<div id="flow-meeting">
配下に下記の記述を追加して、BodyPixによるぼかし前とぼかし後の映像を会議実施画面に表示するようにします。
<p><button id='pix-start'>start</button></p> <div> <video id='input' width="320" height="240" autoplay muted playsinline style="border:solid 1px black;box-sizing:content-box;display:inline-block;transform:scaleX(-1);"></video><span style="position:relative;display:inline-block;"><span id='loadingicon' class="fa fa-spinner fa-spin" style="position:absolute;font-size:100px;margin:70px 110px;display:none;"></span><canvas id='output' width="320" height="240" style="border:solid 1px black;box-sizing:content-box;"></canvas></span> </div>
BodyPixのインポート
demos/browser/app/meetingV2/meetingV2.ts
に次の記述を追加して、BodyPixをインポートして使えるようにします。
import * as bodyPix from '@tensorflow-models/body-pix';
@tensorflow/tfjs
のインポート
demos/browser/app/meetingV2/meetingV2.ts
に次の記述を追加します。
import * as tf from '@tensorflow/tfjs'; console.log('Using TensorFlow backend: ', tf.getBackend());
@tensorflow/tfjs
をインポートしない場合、アプリ上でBodyPixによる処理を行おうとすると次のエラーが発生したため、必要な記述となっています。
engine.js:269 Uncaught (in promise) Error: No backend found in registry. at Engine.getSortedBackends (engine.js:269) at Engine.initializeBackendsAndReturnBest (engine.js:278) at Engine.get backend [as backend] (engine.js:110) at Engine.makeTensor (engine.js:577) at makeTensor (tensor_ops_util.js:61) at tensor (tensor.js:53) at Module.decodeWeights (io_utils.js:229) at GraphModel.loadSync (graph_model.js:131) at GraphModel.load (graph_model.js:114) at async loadGraphModel (graph_model.js:382)
次の投稿によるとこのエラーを解消するためには@tensorflow/tfjs
のインポートが必要とのことです。
これによりエラーを解消することができました。
処理1および2を開始するイベントリスナーの追加
DemoMeetingApp
クラスのinitEventListeners
メソッドに以下の記述を追加して、[start]ボタンをクリックすると、処理「1. カメラからvideoタグ領域への映像入力」および「2. videoタグ領域の映像の背景をぼかし処理してcanvasタグ領域へ描画」を開始するイベントリスナーがDOMに追加されるようにします。
document.getElementById('pix-start').addEventListener('click', e => { e.preventDefault(); this.startDrawBokehEffect(); });
処理3の実装
DemoMeetingApp
クラスのopenVideoInputFromSelection
メソッドを修正して、既存のドロップダウンリストで選択したデバイスからの入力ではなく、「3. canvasタグ領域の描画をMediaStreamとして取得しビデオタイルに入力する」が行われるようにします。
次の既存の記述をコメントアウトして削除します。
//const device = this.videoInputSelectionToDevice(this.selectedVideoInput);
次の記述を追加して、BodyPixによる背景ぼかし映像のcanvas
タグ(id:output
)への描写をMediaStreamとして取得し、ビデオタイルへの入力に渡すようにします。
interface CanvasElement extends HTMLCanvasElement { captureStream(frameRate?: number): MediaStream; } const device = (<CanvasElement>document.getElementById('output')).captureStream();
CanvasElement
型をあえて定義しているのは、定義しない場合だとcaptureStream()
によるMediaStreamの取得でTypeScriptの静的エラーとなるためです。
HTMLCanvasElement.captureStream() - Web API | MDNによると、メソッドしては持ってはいるのですが、実験的な機能であるためまだ利用中のTypeScriptバージョンで対応していないようです。よって、次のStack Overflowの投稿を参考に型定義を行いました。
videoInputSelectionToDevice()
メソッドの削除
ビデオタイルへの入力はドロップダウンリストでの選択でなく既定で背景ぼかし映像が入力されるようにしたため、使用しなくなったDemoMeetingApp
クラスのvideoInputSelectionToDevice()
メソッドをコメントアウトにより削除します。
/* private videoInputSelectionToDevice(value: string): Device { if (this.isRecorder() || this.isBroadcaster()) { return null; } if (value === 'Blue') { return DefaultDeviceController.synthesizeVideoDevice('blue'); } else if (value === 'SMPTE Color Bars') { return DefaultDeviceController.synthesizeVideoDevice('smpte'); } else if (value === 'None') { return null; } return value; } */
処理1および2の実装
このBodyPixによる背景ぼかし処理の部分は、次の記事を大いに参考にさせて頂きました。記事内にコードの詳しい解説もあるので合わせて参照ください。
DemoMeetingApp
クラスに次のsetupCamera()
、segmentBody()
、loadingc()
およびstartDrawBokehEffect()
メソッドの記述を追加します。これらメソッドにより「1. カメラからvideoタグ領域への映像入力」および「2. videoタグ領域の映像の背景をぼかし処理してcanvasタグ領域へ描画」の処理を実装します。
async setupCamera(videoElement: any) { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 320, height: 240, }, audio: false, }); videoElement.srcObject = stream; return new Promise(resolve => { videoElement.onloadedmetadata = () => { videoElement.play(); resolve(); }; }); } segmentBody(input: any, output: any, bodypixnet: any) { async function renderFrame() { const segmentation = await bodypixnet.segmentPerson(input); const backgroundBlurAmount = 3; const edgeBlurAmount = 3; const flipHorizontal = true; bodyPix.drawBokehEffect( output, input, segmentation, backgroundBlurAmount, edgeBlurAmount, flipHorizontal ); requestAnimationFrame(renderFrame); } renderFrame(); } loading(onoff: any) { document.getElementById('loadingicon').style.display = onoff ? 'inline' : 'none'; } async startDrawBokehEffect() { this.loading(true); const input = document.getElementById('input'); if ((input as HTMLMediaElement).srcObject) { (input as HTMLMediaElement).srcObject = null; } else { const output = document.getElementById('output'); await this.setupCamera(input); const bodypixnet = await bodyPix.load(); this.segmentBody(input, output, bodypixnet); } this.loading(false); }
修正したデモアプリの実行
% npm run start
ビデオ会議の背景をぼかすことができました。
次回(後編)に行いたい対応
今回(前編)は背景ぼかしができることを簡単に確認することが目的の記事でした。次回(後編)は次のような実装対応を行いたいと思います。
videoタグ領域、canvasタグ領域を使わずビデオタイルに映像入力する
今回はカメラ - videoタグ領域 - 背景ぼかし処理 - canvasタグ領域 - ビデオタイル
という順の処理でビデオタイルに背景ぼかしした映像を入力しましたが、処理が冗長で、そもそもvideoタグ領域とcanvasタグ領域は実際のアプリでは不要のため、次回はビデオタイルに直接映像入力するようにしたいです。
ビデオ会議をすると相手の映像が固まる事象の解消
今回修正したデモアプリを使い複数人でビデオ会議をすると、相手のビデオ出力映像がなぜか固まってしまうため、次回はこの事象を解消したいと思います。
背景ぼかしをするかどうか選択できるようにする
アプリの実際の利用としては必要なのでこれは実装したいです。
(2020/11/18)後編を投稿しました。
おわりに
オープンソースの機械学習モデルであるBodyPixを使ってAmazon Chime SDKのビデオ会議の背景をぼかせることを簡単に確認してみました。
BodyPixを今回初めて使いましたがとても便利ですね。他にも複数人の場合や身体のパーツごとにセグメントしたり、顔だけにぼかしをかけたりと色々できるようなので今後機会があれば使っていきたいと思います。
参考
以上